#include "GPIO.hpp"
#include "System.hpp"

static const int external_interrupt_channel_count = 16;

struct ExternalInterruptResource {
    Pin *instance;
    System::InterruptChannel interrupt_channel;
    Pin::EventCallback rising_edge_callback;
    Pin::EventCallback falling_edge_callback;
} external_interrupt_resources[external_interrupt_channel_count] = {
    [0] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI0, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [1] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI1, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [2] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI2, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [3] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI3, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [4] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI4, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [5] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI9_5, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [6] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI9_5, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [7] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI9_5, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [8] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI9_5, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [9] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI9_5, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [10] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI15_10, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [11] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI15_10, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [12] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI15_10, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [13] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI15_10, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [14] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI15_10, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
    [15] = {.instance = nullptr, .interrupt_channel = System::InterruptChannel::EXTI15_10, .rising_edge_callback = nullptr, .falling_edge_callback = nullptr, },
};

struct PortResource {
    ST::Port *instance;
    GPIO_Register_t *reg;
    System::ClockChannel clock_channel;
    int width;
} port_resources[] = {
    /* GPIOA. */
    [0] = {
        .instance = nullptr,
        .reg = (GPIO_Register_t*)0x40020000,
        .clock_channel = System::ClockChannel::GPIOA,
        .width = 16,
    },
    /* GPIOB. */
    [1] = {
        .instance = nullptr,
        .reg = (GPIO_Register_t*)0x40020400,
        .clock_channel = System::ClockChannel::GPIOB,
        .width = 16,
    },
    /* GPIOC. */
    [2] = {
        .instance = nullptr,
        .reg = (GPIO_Register_t*)0x40020800,
        .clock_channel = System::ClockChannel::GPIOC,
        .width = 16,
    },
    /* GPIOD. */
    [3] = {
        .instance = nullptr,
        .reg = (GPIO_Register_t*)0x40020c00,
        .clock_channel = System::ClockChannel::GPIOD,
        .width = 16,
    },
    /* GPIOE. */
    [4] = {
        .instance = nullptr,
        .reg = (GPIO_Register_t*)0x40021000,
        .clock_channel = System::ClockChannel::GPIOE,
        .width = 16,
    },
    /* GPIOF not support. */
    [5] = {0,},
    /* GPIOG not support. */
    [6] = {0,},
    /* GPIOH. */
    [7] = {
        .instance = nullptr,
        .reg = (GPIO_Register_t*)0x40021c00,
        .clock_channel = System::ClockChannel::GPIOH,
        .width = 16,
    },
};

class __Pin : public ST::Pin {
public:
    __Pin(int port_number,int pin_number);
    virtual ~__Pin();
    virtual bool open(void);
    virtual bool close(void);
    virtual void set(bool status);
    virtual bool get(void);

    virtual bool setMode(Mode mode);
    virtual Mode getMode(void);
    virtual bool setDriveType(DriveType drive_type);
    virtual DriveType getDriveType(void);
    virtual bool setPullType(PullType pull_type);
    virtual PullType getPullType(void);
    virtual bool setAlternateFunction(bool status);
    virtual bool getAlternateFunction(void);
    virtual bool setAlternateFunctionChannel(int alternate_function_channel);
    virtual int getAlternateFunctionChannel(void);

    virtual bool addEventCallback(EventType event_type,EventCallback event_callback);
    virtual bool removeEventCallback(EventType event_type);
private:
    int port_number;
    int pin_number;
    Mode mode;
    DriveType drive_type;
    PullType pull_type;
    bool alternate_function_status;
    int alternate_function_channel;
    bool status;
};

class __Port : public ST::Port {
public:
    __Port(int port_number);
    virtual ~__Port();
    virtual bool open(void);
    virtual bool close(void);
    virtual int read(void *buffer,int size);
    virtual int write(void *buffer,int size);
    virtual ST::Pin *getPin(int pin_number);
private:
    int port_number;
    ST::Pin **pins;
};

/* Pin. */

__Pin::__Pin(int port_number,int pin_number){
    this->port_number = port_number;
    this->pin_number = pin_number;
    /* Default configuration. */
    this->status = false;
    this->mode = Mode::Input;
    this->drive_type = DriveType::PushPull;
    this->pull_type = PullType::None;
    this->alternate_function_status = false;
}

__Pin::~__Pin(){

}

bool __Pin::open(void){
    uint32_t mode_reg = 0x00000000;
    uint32_t otype_reg = 0x00000000;
    uint32_t ospeed_reg = 0x00000003;
    uint32_t pupd_reg = 0x00000000;

    /* Configurate MODER. */
    if ( this->alternate_function_status ){
        port_resources[this->port_number].reg->AFR[this->pin_number / 8] &= ~(0x0f << ((this->pin_number % 8) * 4));
        port_resources[this->port_number].reg->AFR[this->pin_number / 8] |= ((this->alternate_function_channel & 0x0f) << ((this->pin_number % 8) * 4));
        mode_reg = 0b10;
    }else{
        switch ( this->mode ){
            case Mode::Output: mode_reg = 0b01; break;
            case Mode::Input: 
            default: mode_reg = 0x00000000; break;
        }
    }
    port_resources[this->port_number].reg->MODER &= ~(0b11 << (this->pin_number * 2));
    port_resources[this->port_number].reg->MODER |= (mode_reg << (this->pin_number * 2));

    /* Configurate OTYPER. */
    switch ( this->drive_type ){
        case DriveType::OpenDrain: otype_reg = 0b01; break;
        case DriveType::PushPull:
        default: otype_reg = 0b00; break;
    }
    port_resources[this->port_number].reg->OTYPER &= ~(0b01 << this->pin_number);
    port_resources[this->port_number].reg->OTYPER |= (otype_reg << this->pin_number);

    /* Configurate OSPEEDR. default very high speed. */
    port_resources[this->port_number].reg->OSPEEDR &= ~(0b11 << (this->pin_number * 2));
    port_resources[this->port_number].reg->OSPEEDR |= (ospeed_reg << (this->pin_number * 2));

    /* Configurate PUPDR. */
    switch ( this->pull_type ){
        case PullType::PullUp: pupd_reg = 0b01; break;
        case PullType::PullDown: pupd_reg = 0b10; break;
        case PullType::None: 
        default: pupd_reg = 0b00; break;
    }
    port_resources[this->port_number].reg->PUPDR &= ~(0b11 << (this->pin_number * 2));
    port_resources[this->port_number].reg->PUPDR |= (pupd_reg << (this->pin_number * 2));

    /* Set open flag. */
    this->status = true;
    return true;

}

bool __Pin::close(void){
    this->status = false;
    return true;
}

void __Pin::set(bool status){
    if ( this->status ){
        if ( status ){
            port_resources[this->port_number].reg->BSRR = (0x01 << this->pin_number);
        }else{
            port_resources[this->port_number].reg->BSRR = (0x01 << (this->pin_number + 16));
        }
    }
}

bool __Pin::get(void){
    if ( (port_resources[this->port_number].reg->IDR & ((uint32_t)0x01 << this->pin_number)) != 0 ){
        return true;
    }else{
        return false;
    }
}

bool __Pin::setMode(Mode mode){
    this->mode = mode;
    if ( this->status ){
        if ( !this->close() ) return false;
        if ( !this->open() ) return false;
        if ( this->getMode() != mode ) return false;
    }
    return true;
}

Pin::Mode __Pin::getMode(void){
    if ( !this->status ) return this->mode;
    uint32_t mode_reg = port_resources[this->port_number].reg->MODER;
    mode_reg = ((mode_reg >> (this->pin_number * 2)) & 0x03);
    switch ( mode_reg ){
        case 0b00: this->mode = Mode::Input; break;
        case 0b01: this->mode = Mode::Output; break;
        case 0b10: this->mode = Mode::Output; break;
        case 0b11: this->mode = Mode::Input; break;
        default: this->mode = Mode::Input; break;
    }
    return this->mode;
}

bool __Pin::setDriveType(DriveType drive_type){
    this->drive_type = drive_type;
    if ( this->status ){
        if ( !this->close() ) return false;
        if ( !this->open() ) return false;
        if ( this->getDriveType() != drive_type ) return false;
    }
    return true;
}

Pin::DriveType __Pin::getDriveType(void){
    if ( !this->status ) return this->drive_type;
    uint32_t otype_reg = port_resources[this->port_number].reg->OTYPER;
    otype_reg = ((otype_reg >> this->pin_number) & 0x01);
    switch ( otype_reg ){
        case 0b00: this->drive_type = DriveType::PushPull; break;
        case 0b01: this->drive_type = DriveType::OpenDrain; break;
        default: this->drive_type = DriveType::PushPull; break;
    }
    return this->drive_type;
}

bool __Pin::setPullType(PullType pull_type){
    this->pull_type = pull_type;
    if ( this->status ){
        if ( !this->close() ) return false;
        if ( !this->open() ) return false;
        if ( this->getPullType() != pull_type ) return false;
    }
    return true;
}

Pin::PullType __Pin::getPullType(void){
    if ( !this->status ) return this->pull_type;
    uint32_t pupd_reg = port_resources[this->port_number].reg->PUPDR;
    pupd_reg = ((pupd_reg >> (this->pin_number * 2)) & 0x03);
}

bool __Pin::setAlternateFunction(bool status){
    this->alternate_function_status = status;
    if ( this->status ){
        if ( !this->close() ) return false;
        if ( !this->open() ) return false;
        if ( this->getAlternateFunction() != status ) return false;
    }
    return true;
}

bool __Pin::getAlternateFunction(void){
    if ( !this->status ) return this->alternate_function_status;
    uint32_t mode_reg = port_resources[this->port_number].reg->MODER;
    mode_reg = ((mode_reg >> (this->pin_number * 2)) & 0x03);
    if ( mode_reg == 0b10 ){
        this->alternate_function_status = true;
    }else{
        this->alternate_function_status = false;
    }
    return this->alternate_function_status;
}

bool __Pin::setAlternateFunctionChannel(int alternate_function_channel){
    this->alternate_function_channel = alternate_function_channel;
    if ( this->status ){
        if ( !this->close() ) return false;
        if ( !this->open() ) return false;
        if ( this->getAlternateFunctionChannel() != alternate_function_channel ) return false;
    }
    return true;
}

int __Pin::getAlternateFunctionChannel(void){
    if ( !this->status ) return this->alternate_function_channel;
    uint32_t alternate_function_register;
    alternate_function_register = port_resources[this->port_number].reg->AFR[this->pin_number / 8];
    alternate_function_register = alternate_function_register >> ((4 * (this->pin_number % 8)) & 0x0f);
    this->alternate_function_channel = (uint32_t)alternate_function_channel;
    return this->alternate_function_channel;
}

bool __Pin::addEventCallback(EventType event_type,EventCallback event_callback){
    EXTI_Register_t *EXTI_register = (EXTI_Register_t*)0x40013c00;
    /* Check external interrupt channel occupancy. */
    if ( external_interrupt_resources[this->pin_number].instance != nullptr && external_interrupt_resources[this->pin_number].instance != this ){
        return false;
    }
    /* Occupying the channel. */
    external_interrupt_resources[this->pin_number].instance = this;

    uint8_t type_map = (uint8_t)event_type;
    if ( (type_map & (uint8_t)EventType::RisingEdge) != 0 ) external_interrupt_resources[this->pin_number].rising_edge_callback = event_callback;
    if ( (type_map & (uint8_t)EventType::FallingEdge) != 0 ) external_interrupt_resources[this->pin_number].falling_edge_callback = event_callback;

    /* Enable external interrupt. */

    System::InterruptChannel interrupt_channel;
    int exti_channel_number = (this->pin_number < external_interrupt_channel_count) ? this->pin_number : -1;
    if ( exti_channel_number < 0 ) return false;
    interrupt_channel = external_interrupt_resources[exti_channel_number].interrupt_channel;
    
    /* Configurate EXTI. */
    if ( (type_map & (uint8_t)EventType::RisingEdge) != 0 ) EXTI_register->RTSR |= (0x01 << this->pin_number);
    if ( (type_map & (uint8_t)EventType::FallingEdge) != 0 ) EXTI_register->FTSR |= (0x01 << this->pin_number);
    EXTI_register->IMR |= (0x01 << this->pin_number);
    System::setInterruptChannelStatus(interrupt_channel,true);
    void EXTI_IRQHandler(void *user_data);
    System::addInterruptCallback(interrupt_channel,EXTI_IRQHandler,&external_interrupt_resources[this->pin_number]);
    return true;
}

bool __Pin::removeEventCallback(EventType event_type){
    EXTI_Register_t *EXTI_register = (EXTI_Register_t*)0x40013c00;
    if ( external_interrupt_resources[this->pin_number].instance != this ) return false;

    uint8_t type_map = (uint8_t)event_type;
    if ( (type_map & (uint8_t)EventType::RisingEdge) != 0 ) external_interrupt_resources[this->pin_number].rising_edge_callback = nullptr;
    if ( (type_map & (uint8_t)EventType::FallingEdge) != 0 ) external_interrupt_resources[this->pin_number].falling_edge_callback = nullptr;
    
    if ( external_interrupt_resources[this->pin_number].rising_edge_callback == nullptr && external_interrupt_resources[this->pin_number].falling_edge_callback == nullptr ){
        EXTI_register->IMR &= ~(0x01 << this->pin_number);
        external_interrupt_resources[this->pin_number].instance = nullptr;
    }

    return true;
}

/* Port. */

__Port::__Port(int port_number){
    this->port_number = port_number;
    this->pins = new ST::Pin*[port_resources[this->port_number].width];
    for ( int index = 0; index < port_resources[this->port_number].width; index++ ){
        this->pins[index] = nullptr;
    }
}

__Port::~__Port(){
    if ( this->pins != nullptr ){
        for ( int index = 0; index < port_resources[this->port_number].width; index++ ){
            if ( this->pins[index] != nullptr ){
                this->pins[index]->close();
                delete this->pins[index];
                this->pins[index] = nullptr;
            }
        }
        delete[] this->pins;
        this->pins = nullptr;
    }
}

bool __Port::open(void){
    if ( !System::setClockChannelStatus(port_resources[this->port_number].clock_channel,true) ) return false;
    return true;
}

bool __Port::close(void){
    return true;
}

int __Port::read(void *buffer,int size){
    unsigned char *uc_buffer = (unsigned char*)buffer;
    uint32_t input_data_reg = port_resources[this->port_number].reg->IDR;
    int port_size = ( (port_resources[this->port_number].width % 8) == 0 ) ? (port_resources[this->port_number].width / 8) : (port_resources[this->port_number].width / 8 + 1);
    if ( size > port_size ) size = port_size;
    for ( int n = size; n > 0; n-- ){
        uc_buffer[n] = (input_data_reg & 0xff);
        input_data_reg >>= 8;
    }
    return size;
}

int __Port::write(void *buffer,int size){
    unsigned char *uc_buffer = (unsigned char*)buffer;
    uint32_t output_data_reg = 0x00000000;
    int port_size = ( (port_resources[this->port_number].width % 8) == 0 ) ? (port_resources[this->port_number].width / 8) : (port_resources[this->port_number].width / 8 + 1);
    if ( size > port_size ) size = port_size;
    for ( int n = size; n > 0; n-- ){
        output_data_reg |= uc_buffer[n];
        output_data_reg <<= 8;
    }
    port_resources[this->port_number].reg->ODR = output_data_reg;
    return size;
}

ST::Pin *__Port::getPin(int pin_number){
    if ( pin_number >= port_resources[this->port_number].width ) return nullptr;
    if ( this->pins[pin_number] == nullptr ){
        this->pins[pin_number] = new __Pin(this->port_number,pin_number);
    }
    return this->pins[pin_number];
}

ST::Port *ST::Port::getInstance(int port_number){
    if ( port_number >= (sizeof(port_resources) / sizeof(PortResource)) ) return nullptr;
    if ( port_resources[port_number].instance == nullptr ){
        port_resources[port_number].instance = new __Port(port_number);
    }
    return port_resources[port_number].instance;
}

bool ST::Port::freeInstance(ST::Port *instance){
    if ( instance == nullptr ) return false;
    for ( int index = 0; index < (sizeof(port_resources) / sizeof(PortResource)); index++ ){
        if ( port_resources[index].instance == instance ){
            if ( !port_resources[index].instance->close() ) return false;
            delete port_resources[index].instance;
            port_resources[index].instance = nullptr;
            return true;
        }
    }
    return false;
}

void EXTI_IRQHandler(void *user_data){
    EXTI_Register_t *EXTI_register = (EXTI_Register_t*)0x40013c00;
    ExternalInterruptResource *external_interrupt_resource = (ExternalInterruptResource*)user_data;
    int exti_channel = (external_interrupt_resource - external_interrupt_resources);
    if ( ( EXTI_register->PR & (0x01 << exti_channel) ) != 0UL ){
        if ( external_interrupt_resources[exti_channel].instance->get() ){
            if ( external_interrupt_resources[exti_channel].rising_edge_callback != nullptr ){
                external_interrupt_resources[exti_channel].rising_edge_callback();
            }
        }else{
            if ( external_interrupt_resources[exti_channel].falling_edge_callback != nullptr ){
                external_interrupt_resources[exti_channel].falling_edge_callback();
            }
        }
        EXTI_register->PR = (0x01 << exti_channel);
    }
    return;
}